package com.googlecode.eventio.annotation;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import com.googlecode.eventio.EventDispatcher;
import com.googlecode.eventio.Reaction;

/**
 * Adapter over {@link EventDispatcher} that makes static event routing neater
 * (i.e. event routing that does not need
 * {@link EventDispatcher#once(Object, Reaction)}) . This dispatcher routes
 * events based on class, and reactions are defined using the {@link Subscribe}
 * annotation. A subscriber class can only have one method annotated for a given
 * type.
 * 
 * <p>
 * 
 * It is worth noticing that, unlike Guava EventBus, inheritance is not taken
 * into account when matching events. This is implied by {@link EventDispatcher}
 * hashCode and equals event matching.
 * 
 * @author mofleury
 * 
 */
public class StaticEventDispatcher {

	private final EventDispatcher delegateDispatcher;

	private final Map<Integer, Collection<EventReactionPair>> registeredSubscribers = new HashMap<Integer, Collection<EventReactionPair>>();

	public StaticEventDispatcher(EventDispatcher delegateDispatcher) {
		super();
		this.delegateDispatcher = delegateDispatcher;
	}

	/**
	 * Emits an event. This method is a shortcut to </p> <code>
	 * delegateDispatcher.emit(event.getClass(), event);
	 * </code>
	 * <p>
	 * This implies that it is possible to mix static and dynamic event matching
	 * by registering to event classes on the delegate {@link EventDispatcher}
	 * directly.
	 * 
	 * @param event
	 *            the event to be emitted
	 */
	public void emit(Object event) {

		delegateDispatcher.emit(event.getClass(), event);
	}

	/**
	 * register the given instance to the delegate {@link EventDispatcher} in
	 * the following way: Every method annotated with {@link Subscribe} will be
	 * converted into a {@link Reaction} ; The event used for registration is
	 * the method parameter class.
	 * 
	 * @param subscriber
	 */
	public void register(final Object subscriber) {

		// TODO use a cache to make repeated subcriptions more efficient
		Method[] methods = subscriber.getClass().getMethods();

		Set<Class<?>> argumentClasses = new HashSet<Class<?>>();

		Collection<EventReactionPair> registeredReactions = new ArrayList<EventReactionPair>();

		for (final Method method : methods) {
			if (method.getAnnotation(Subscribe.class) != null) {

				if (!method.getReturnType().equals(void.class)) {
					throw new IllegalArgumentException(
							"found @Subscribe annotation on invalid method: " + method
									+ " has non-void return type. found " + method.getReturnType());
				}

				Class<?>[] parameters = method.getParameterTypes();
				if (parameters.length != 1) {
					throw new IllegalArgumentException(
							"found @Subscribe annotation on invalid method: " + method
									+ " has more than one parameter. Found " + parameters);
				}

				Class<?> eventClass = parameters[0];

				if (!argumentClasses.add(eventClass)) {
					throw new IllegalArgumentException(
							"More than one subcribe method found with parameter " + eventClass
									+ " in subscriber class " + subscriber.getClass());
				}

				Reaction<Object> reaction = new Reaction<Object>() {
					@Override
					public void react(Object input) {
						try {
							method.invoke(subscriber, input);
						} catch (IllegalArgumentException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						} catch (IllegalAccessException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						} catch (InvocationTargetException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
				};

				delegateDispatcher.on(eventClass, reaction);

				registeredReactions.add(new EventReactionPair(eventClass, reaction));

			}
		}

		registeredSubscribers.put(Integer.valueOf(System.identityHashCode(subscriber)),
				registeredReactions);

		if (argumentClasses.isEmpty()) {
			throw new RuntimeException("No react method with annotation @Subscribe.");
		}
	}

	public void unregister(Object subscriber) {
		Collection<EventReactionPair> reactions = registeredSubscribers.get(System
				.identityHashCode(subscriber));

		if (reactions == null) {
			throw new IllegalArgumentException("No reactions known for subscriber " + subscriber);
		}

		for (EventReactionPair p : reactions) {
			delegateDispatcher.remove(p.event, p.reaction);
		}

	}

	private static class EventReactionPair {
		Class<?> event;
		Reaction<?> reaction;

		public EventReactionPair(Class<?> event, Reaction<?> reaction) {
			super();
			this.event = event;
			this.reaction = reaction;
		}

	}

}
